﻿using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Uow;

namespace Xms.Abp.Caching;

public class KeyValueCache<TCacheItem> : IKeyValueCache<TCacheItem>
{
    protected IAbpLazyServiceProvider LazyServiceProvider { get; }

    public const string UowCacheName = "AbpDistributedCache";

    public ILogger<KeyValueCache<TCacheItem>> Logger { get; set; }

    protected string CacheName { get; set; }

    protected string CacheKey { get; set; }

    protected ICancellationTokenProvider CancellationTokenProvider { get; }

    protected IDistributedCacheSerializer Serializer { get; }

    protected IServiceScopeFactory ServiceScopeFactory { get; }

    protected IUnitOfWorkManager UnitOfWorkManager { get; }

    protected static SemaphoreSlim SyncSemaphore { get; } = new(1, 1);

    protected DistributedCacheEntryOptions DefaultCacheOptions;

    protected bool MultiTenancyIsEnable { get; set; }

    protected ICacheProvider CacheProvider { get; }
    protected IXmsCache _cache;
    protected IXmsCache Cache
    {
        get
        {
            if (_cache == null)
            {
                _cache = CacheProvider.GetCache();
            }
            return _cache;
        }
    }

    private readonly AbpDistributedCacheOptions _distributedCacheOption;
    public KeyValueCache(
        IAbpLazyServiceProvider lazyServiceProvider,
        ICacheProvider cacheProvider,
        string cacheName = null)
    {
        LazyServiceProvider = lazyServiceProvider;

        Logger = LazyInject<ILogger<KeyValueCache<TCacheItem>>>();

        CacheProvider = cacheProvider;
        _distributedCacheOption = LazyInject<IOptions<AbpDistributedCacheOptions>>().Value;
        CancellationTokenProvider = LazyInject<ICancellationTokenProvider>();
        Serializer = LazyInject<IDistributedCacheSerializer>();
        ServiceScopeFactory = LazyInject<IServiceScopeFactory>();
        UnitOfWorkManager = LazyInject<IUnitOfWorkManager>();

        MultiTenancyIsEnable = !typeof(TCacheItem).IsDefined(typeof(IgnoreMultiTenancyAttribute), true);

        CacheName = cacheName ?? CacheName;
        CacheKey = LazyInject<IDistributedCacheKeyNormalizer>()
            .NormalizeKey(new DistributedCacheKeyNormalizeArgs(null, CacheName, !MultiTenancyIsEnable));
    }

    public TService LazyInject<TService>()
    {
        return LazyServiceProvider.LazyGetRequiredService<TService>();
    }

    protected virtual string NormalizeKey(string key)
    {
        return CacheKey + key;
    }

    public async Task SetAbsoluteAsync(string key, TCacheItem value, DateTimeOffset expiration, bool? hideErrors = null, bool considerUow = false, CancellationToken cancellationToken = default)
    {
        if (expiration < DateTimeOffset.Now)
        {
            await RemoveAsync(key, hideErrors, considerUow, cancellationToken);
        }

        await SetAsync(key, value, new DistributedCacheEntryOptions { AbsoluteExpiration = expiration }, hideErrors, considerUow, cancellationToken);
    }
    public async Task SetSlidingAsync(string key, TCacheItem value, TimeSpan expiration, bool? hideErrors = null, bool considerUow = false, CancellationToken cancellationToken = default)
    {
        await SetAsync(key, value, new DistributedCacheEntryOptions { SlidingExpiration = expiration }, hideErrors, considerUow, cancellationToken);
    }

    public async Task UpdateAsync(string key, TCacheItem value, bool? hideErrors = null, bool considerUow = false, CancellationToken cancellationToken = default)
    {
        key = NormalizeKey(key);
        hideErrors ??= _distributedCacheOption.HideErrors;
        try
        {
            if (await Cache.KeyExistsAsync(key, cancellationToken))
            {
                //TODO: Set Field
                await Cache.SetHashAsync(key, Cache.DataKeyName, Serializer.Serialize(value));
                await Cache.RefreshAsync(key, cancellationToken);
            }
            else
            {
                throw new KeyNotFoundException(nameof(key));
            }
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
            }
            else
            {
                throw;
            }
        }
    }

    public async Task SetAsync(string key, TCacheItem value, DateTimeOffset absoluteExpiration, TimeSpan slidingExpiration, bool? hideErrors = null, bool considerUow = false, CancellationToken cancellationToken = default)
    {
        await SetAsync(key, value, new DistributedCacheEntryOptions { AbsoluteExpiration = absoluteExpiration, SlidingExpiration = slidingExpiration }, hideErrors, considerUow, cancellationToken);
    }

    public async Task<bool> KeyExistsAsync(string key, CancellationToken cancellationToken = default)
    {
        return await Cache.KeyExistsAsync(NormalizeKey(key), cancellationToken);
    }

    public async Task<DistributedCacheEntryOptions> GetExpirationFromCache(string key, CancellationToken cancellationToken = default)
    {
        return await Cache.GetExpirationFromCache(NormalizeKey(key), cancellationToken);
    }


    public virtual async Task<KeyValuePair<string, TCacheItem>[]> GetManyAsync(
        IEnumerable<string> keys,
        bool? hideErrors = null,
        bool considerUow = false,
        CancellationToken cancellationToken = default)
    {
        var keyArray = keys.ToArray();

        if (Cache is not ICacheSupportsMultipleItems cacheSupportsMultipleItems)
        {
            return await GetManyFallbackAsync(
                keyArray,
                hideErrors,
                considerUow,
                cancellationToken
            );
        }

        var notCachedKeys = new List<string>();
        var cachedValues = new List<KeyValuePair<string, TCacheItem>>();
        if (ShouldConsiderUow(considerUow))
        {
            var uowCache = GetUnitOfWorkCache();
            foreach (var key in keyArray)
            {
                var item = uowCache.GetOrDefault(key);
                var value = item.IsRemoved ? default : item.Value;
                if (Equals(value, default(TCacheItem)))
                {
                    cachedValues.Add(new KeyValuePair<string, TCacheItem>(key, value));
                }
            }

            notCachedKeys = keyArray.Except(cachedValues.Select(x => x.Key)).ToList();
            if (!notCachedKeys.Any())
            {
                return cachedValues.ToArray();
            }
        }

        hideErrors ??= _distributedCacheOption.HideErrors;
        byte[][] cachedBytes;

        var readKeys = notCachedKeys.Any() ? notCachedKeys.ToArray() : keyArray;

        try
        {
            cachedBytes = await cacheSupportsMultipleItems.GetManyAsync(
                readKeys.Select(NormalizeKey),
                CancellationTokenProvider.FallbackToProvider(cancellationToken)
            );
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
                return ToCacheItemsWithDefaultValues(keyArray);
            }

            throw;
        }

        return cachedValues.Concat(ToCacheItems(cachedBytes, readKeys)).ToArray();
    }

    protected virtual async Task<KeyValuePair<string, TCacheItem>[]> GetManyFallbackAsync(
        string[] keys,
        bool? hideErrors = null,
        bool considerUow = false,
        CancellationToken cancellationToken = default)
    {
        hideErrors ??= _distributedCacheOption.HideErrors;

        try
        {
            var result = new List<KeyValuePair<string, TCacheItem>>();

            foreach (var key in keys)
            {
                result.Add(new KeyValuePair<string, TCacheItem>(
                    key,
                    await GetAsync(key, false, considerUow, cancellationToken: cancellationToken))
                );
            }

            return result.ToArray();
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
                return ToCacheItemsWithDefaultValues(keys);
            }

            throw;
        }
    }

    public virtual async Task<TCacheItem> GetAsync(
        string key,
        bool? hideErrors = null,
        bool considerUow = false,
        CancellationToken cancellationToken = default)
    {
        hideErrors ??= _distributedCacheOption.HideErrors;

        if (ShouldConsiderUow(considerUow))
        {
            var item = GetUnitOfWorkCache().GetOrDefault(key);
            var value = item.IsRemoved ? default : item.Value;
            if (Equals(value, default(TCacheItem)))
            {
                return value;
            }
        }

        byte[] cachedBytes;

        try
        {
            cachedBytes = await Cache.GetAsync(
                NormalizeKey(key),
                CancellationTokenProvider.FallbackToProvider(cancellationToken)
            );
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
                return default;
            }

            throw;
        }

        if (cachedBytes == null)
        {
            return default;
        }

        return Serializer.Deserialize<TCacheItem>(cachedBytes);
    }

    public virtual async Task<TCacheItem> GetOrAddAsync(
        string key,
        Func<Task<TCacheItem>> factory,
        Func<DistributedCacheEntryOptions> optionsFactory = null,
        bool? hideErrors = null,
        bool considerUow = false,
        CancellationToken cancellationToken = default)
    {
        cancellationToken = CancellationTokenProvider.FallbackToProvider(cancellationToken);
        var value = await GetAsync(key, hideErrors, considerUow, cancellationToken);
        if (value != null)
        {
            return value;
        }

        using (await SyncSemaphore.LockAsync(cancellationToken))
        {
            value = await GetAsync(key, hideErrors, considerUow, cancellationToken);
            if (value != null)
            {
                return value;
            }

            value = await factory();

            if (ShouldConsiderUow(considerUow))
            {
                var uowCache = GetUnitOfWorkCache();
                if (uowCache.TryGetValue(key, out var item))
                {
                    item.SetValue(value);
                }
                else
                {
                    uowCache.Add(key, new XmsUnitOfWorkCacheItem<TCacheItem>(value));
                }
            }

            await SetAsync(key, value, optionsFactory?.Invoke(), hideErrors, considerUow, cancellationToken);
        }

        return value;
    }


    public async Task<KeyValuePair<string, TCacheItem>[]> GetOrAddManyAsync(
        IEnumerable<string> keys,
        Func<IEnumerable<string>, Task<List<KeyValuePair<string, TCacheItem>>>> factory,
        Func<DistributedCacheEntryOptions> optionsFactory = null,
        bool? hideErrors = null,
        bool considerUow = false,
        CancellationToken cancellationToken = default)
    {
        KeyValuePair<string, TCacheItem>[] result;
        var keyArray = keys.ToArray();

        if (Cache is not ICacheSupportsMultipleItems cacheSupportsMultipleItems)
        {
            result = await GetManyFallbackAsync(
                keyArray,
                hideErrors,
                considerUow, cancellationToken);
        }
        else
        {
            var notCachedKeys = new List<string>();
            var cachedValues = new List<KeyValuePair<string, TCacheItem>>();
            if (ShouldConsiderUow(considerUow))
            {
                var uowCache = GetUnitOfWorkCache();
                foreach (var key in keyArray)
                {
                    var item = uowCache.GetOrDefault(key);
                    var value = item.IsRemoved ? default : item.Value;
                    if (Equals(value, default(TCacheItem)))
                    {
                        cachedValues.Add(new KeyValuePair<string, TCacheItem>(key, value));
                    }
                }

                notCachedKeys = keyArray.Except(cachedValues.Select(x => x.Key)).ToList();
                if (!notCachedKeys.Any())
                {
                    return cachedValues.ToArray();
                }
            }

            hideErrors ??= _distributedCacheOption.HideErrors;
            byte[][] cachedBytes;

            var readKeys = notCachedKeys.Any() ? notCachedKeys.ToArray() : keyArray;
            try
            {
                cachedBytes = await cacheSupportsMultipleItems.GetManyAsync(readKeys.Select(NormalizeKey), cancellationToken);
            }
            catch (Exception ex)
            {
                if (hideErrors == true)
                {
                    await HandleExceptionAsync(ex);
                    return ToCacheItemsWithDefaultValues(keyArray);
                }

                throw;
            }

            result = cachedValues.Concat(ToCacheItems(cachedBytes, readKeys)).ToArray();
        }

        if (result.All(x => x.Value != null))
        {
            return result;
        }

        var missingKeys = new List<string>();
        var missingValuesIndex = new List<int>();
        for (var i = 0; i < keyArray.Length; i++)
        {
            if (result[i].Value != null)
            {
                continue;
            }

            missingKeys.Add(keyArray[i]);
            missingValuesIndex.Add(i);
        }

        var missingValues = (await factory.Invoke(missingKeys)).ToArray();
        var valueQueue = new Queue<KeyValuePair<string, TCacheItem>>(missingValues);

        await SetManyAsync(missingValues, optionsFactory?.Invoke(), hideErrors, considerUow, cancellationToken);

        foreach (var index in missingValuesIndex)
        {
            result[index] = valueQueue.Dequeue();
        }

        return result;
    }

    public virtual async Task SetAsync(
        string key,
        TCacheItem value,
        DistributedCacheEntryOptions options = null,
        bool? hideErrors = null,
        bool considerUow = false,
        CancellationToken cancellationToken = default)
    {
        async Task SetRealCache()
        {
            hideErrors ??= _distributedCacheOption.HideErrors;

            try
            {
                await Cache.SetAsync(
                    NormalizeKey(key),
                    Serializer.Serialize(value),
                    options ?? DefaultCacheOptions,
                    CancellationTokenProvider.FallbackToProvider(cancellationToken)
                );
            }
            catch (Exception ex)
            {
                if (hideErrors == true)
                {
                    await HandleExceptionAsync(ex);
                    return;
                }

                throw;
            }
        }

        if (ShouldConsiderUow(considerUow))
        {
            var uowCache = GetUnitOfWorkCache();
            if (uowCache.TryGetValue(key, out _))
            {
                uowCache[key].SetValue(value);
            }
            else
            {
                uowCache.Add(key, new XmsUnitOfWorkCacheItem<TCacheItem>(value));
            }

            // ReSharper disable once PossibleNullReferenceException
            UnitOfWorkManager.Current.OnCompleted(SetRealCache);
        }
        else
        {
            await SetRealCache();
        }
    }

    public virtual async Task SetManyAsync(
        IEnumerable<KeyValuePair<string, TCacheItem>> items,
        DistributedCacheEntryOptions options = null,
        bool? hideErrors = null,
        bool considerUow = false,
        CancellationToken cancellationToken = default)
    {
        var itemsArray = items.ToArray();

        if (Cache is not ICacheSupportsMultipleItems cacheSupportsMultipleItems)
        {
            await SetManyFallbackAsync(
                itemsArray,
                options,
                hideErrors,
                considerUow,
                cancellationToken
            );

            return;
        }

        async Task SetRealCache()
        {
            hideErrors ??= _distributedCacheOption.HideErrors;

            try
            {
                await cacheSupportsMultipleItems.SetManyAsync(
                    ToRawCacheItems(itemsArray),
                    options ?? DefaultCacheOptions,
                    CancellationTokenProvider.FallbackToProvider(cancellationToken)
                );
            }
            catch (Exception ex)
            {
                if (hideErrors == true)
                {
                    await HandleExceptionAsync(ex);
                    return;
                }

                throw;
            }
        }

        if (ShouldConsiderUow(considerUow))
        {
            var uowCache = GetUnitOfWorkCache();

            foreach (var pair in itemsArray)
            {
                if (uowCache.TryGetValue(pair.Key, out _))
                {
                    uowCache[pair.Key].SetValue(pair.Value);
                }
                else
                {
                    uowCache.Add(pair.Key, new XmsUnitOfWorkCacheItem<TCacheItem>(pair.Value));
                }
            }

            // ReSharper disable once PossibleNullReferenceException
            UnitOfWorkManager.Current.OnCompleted(SetRealCache);
        }
        else
        {
            await SetRealCache();
        }
    }

    protected virtual async Task SetManyFallbackAsync(
        KeyValuePair<string, TCacheItem>[] items,
        DistributedCacheEntryOptions options = null,
        bool? hideErrors = null,
        bool considerUow = false,
        CancellationToken cancellationToken = default)
    {
        hideErrors ??= _distributedCacheOption.HideErrors;

        try
        {
            foreach (var item in items)
            {
                await SetAsync(
                    item.Key,
                    item.Value,
                    options,
                    false,
                    considerUow,
                    cancellationToken: cancellationToken
                );
            }
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
                return;
            }

            throw;
        }
    }

    public virtual async Task RefreshAsync(
        string key,
        bool? hideErrors = null,
        CancellationToken cancellationToken = default)
    {
        hideErrors ??= _distributedCacheOption.HideErrors;

        try
        {
            await Cache.RefreshAsync(NormalizeKey(key), CancellationTokenProvider.FallbackToProvider(cancellationToken));
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
                return;
            }

            throw;
        }
    }

    public virtual async Task RefreshManyAsync(
        IEnumerable<string> keys,
        bool? hideErrors = null,
        CancellationToken cancellationToken = default)
    {
        hideErrors ??= _distributedCacheOption.HideErrors;

        try
        {
            if (Cache is ICacheSupportsMultipleItems cacheSupportsMultipleItems)
            {
                await cacheSupportsMultipleItems.RefreshManyAsync(keys.Select(NormalizeKey), cancellationToken);
            }
            else
            {
                foreach (var key in keys)
                {
                    await Cache.RefreshAsync(NormalizeKey(key), cancellationToken);
                }
            }
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
                return;
            }

            throw;
        }
    }

    public virtual async Task RemoveAsync(
        string key,
        bool? hideErrors = null,
        bool considerUow = false,
        CancellationToken cancellationToken = default)
    {
        async Task RemoveRealCache()
        {
            hideErrors ??= _distributedCacheOption.HideErrors;

            try
            {
                await Cache.RemoveAsync(NormalizeKey(key), CancellationTokenProvider.FallbackToProvider(cancellationToken));
            }
            catch (Exception ex)
            {
                if (hideErrors == true)
                {
                    await HandleExceptionAsync(ex);
                    return;
                }

                throw;
            }
        }

        if (ShouldConsiderUow(considerUow))
        {
            var uowCache = GetUnitOfWorkCache();
            if (uowCache.TryGetValue(key, out _))
            {
                uowCache[key].RemoveValue();
            }

            // ReSharper disable once PossibleNullReferenceException
            UnitOfWorkManager.Current.OnCompleted(RemoveRealCache);
        }
        else
        {
            await RemoveRealCache();
        }
    }

    public async Task RemoveManyAsync(
        IEnumerable<string> keys,
        bool? hideErrors = null,
        bool considerUow = false,
        CancellationToken cancellationToken = default)
    {
        var keyArray = keys.ToArray();

        if (Cache is ICacheSupportsMultipleItems cacheSupportsMultipleItems)
        {
            async Task RemoveRealCache()
            {
                hideErrors ??= _distributedCacheOption.HideErrors;

                try
                {
                    await cacheSupportsMultipleItems.RemoveManyAsync(
                        keyArray.Select(NormalizeKey), cancellationToken);
                }
                catch (Exception ex)
                {
                    if (hideErrors == true)
                    {
                        await HandleExceptionAsync(ex);
                        return;
                    }

                    throw;
                }
            }

            if (ShouldConsiderUow(considerUow))
            {
                var uowCache = GetUnitOfWorkCache();

                foreach (var key in keyArray)
                {
                    if (uowCache.TryGetValue(key, out _))
                    {
                        uowCache[key].RemoveValue();
                    }
                }

                // ReSharper disable once PossibleNullReferenceException
                UnitOfWorkManager.Current.OnCompleted(RemoveRealCache);
            }
            else
            {
                await RemoveRealCache();
            }
        }
        else
        {
            foreach (var key in keyArray)
            {
                await RemoveAsync(key, hideErrors, considerUow, cancellationToken);
            }
        }
    }

    protected virtual async Task HandleExceptionAsync(Exception ex)
    {
        Logger.LogException(ex, LogLevel.Warning);

        using var scope = ServiceScopeFactory.CreateScope();
        await scope.ServiceProvider
            .GetRequiredService<IExceptionNotifier>()
            .NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning));
    }

    protected virtual KeyValuePair<string, TCacheItem>[] ToCacheItems(byte[][] itemBytes, string[] itemKeys)
    {
        if (itemBytes.Length != itemKeys.Length)
        {
            throw new AbpException("count of the item bytes should be same with the count of the given keys");
        }

        var result = new List<KeyValuePair<string, TCacheItem>>();

        for (int i = 0; i < itemKeys.Length; i++)
        {
            result.Add(
                new KeyValuePair<string, TCacheItem>(
                    itemKeys[i],
                    ToCacheItem(itemBytes[i])
                )
            );
        }

        return result.ToArray();
    }

    [CanBeNull]
    protected virtual TCacheItem ToCacheItem([CanBeNull] byte[] bytes)
    {
        if (bytes == null)
        {
            return default;
        }

        return Serializer.Deserialize<TCacheItem>(bytes);
    }


    protected virtual KeyValuePair<string, byte[]>[] ToRawCacheItems(KeyValuePair<string, TCacheItem>[] items)
    {
        return items
            .Select(i => new KeyValuePair<string, byte[]>(
                    NormalizeKey(i.Key),
                    Serializer.Serialize(i.Value)
                )
            ).ToArray();
    }

    private static KeyValuePair<string, TCacheItem>[] ToCacheItemsWithDefaultValues(string[] keys)
    {
        return keys
            .Select(key => new KeyValuePair<string, TCacheItem>(key, default))
            .ToArray();
    }

    protected virtual bool ShouldConsiderUow(bool considerUow)
    {
        return considerUow && UnitOfWorkManager.Current != null;
    }

    protected virtual string GetUnitOfWorkCacheKey()
    {
        return UowCacheName + CacheName;
    }

    protected virtual Dictionary<string, XmsUnitOfWorkCacheItem<TCacheItem>> GetUnitOfWorkCache()
    {
        if (UnitOfWorkManager.Current == null)
        {
            throw new AbpException($"There is no active UOW.");
        }

        return UnitOfWorkManager.Current.GetOrAddItem(GetUnitOfWorkCacheKey(),
            key => new Dictionary<string, XmsUnitOfWorkCacheItem<TCacheItem>>());
    }
}
